package com.ytraveler.conf.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 在这里我们使用redis来存储token
 * 配置授权服务一个比较重要的方面就是提供一个授权码给一个OAuth客户端（通过 authorization_code 授权类型），
 * 一个授权码的获取是OAuth客户端跳转到一个授权页面，然后通过验证授权之后服务器重定向到OAuth客户端，并且在重定向连接中附带返回一个授权码。
 */
/*@Configuration
@EnableAuthorizationServer*/
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {


   /* @Value("${}")
    private String resource;
   */
    /**
     * AuthorizationServerTokenServices  接口定义了一些操作让你可以对令牌进行一些管理
     * 1.当一个令牌创建的时候必须对其进行保存，这样客户端使用令牌对资源服务器进行请求的时候才可以使用这个令牌
     * 2.当一个令牌有效的时候，可以用来加载身份信息，里面包含了令牌相关权限/
     */
    //认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    //redis连接工厂,我们这里使用redis来存放token
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Autowired
    private DataSource dataSource;

    @Bean
    public RedisTokenStore redisTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

    /**
     * ClientDetailsServiceConfigurer 用来配置客户端详情服务, 客户端详情信息在这里初始化，客户端信息写死或者通过数据库调用详情信息。
     * 这里可以使用JDBC和内存获取客户端的详情服务
     * clientId:用来表示客户的Id
     * secret: 客户端的安全码
     * scope:用来限制客户端的访问范围，如果为null，客户端拥有全部的访问范围
     * authorizedGrantTypes:此客户端可以使用的授权类型，默认为null
     * authorities:此客户端可以使用权限
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //默认值InMemoryTokenStore对于单个服务器是完全正常的（即，在发生故障的情况下，低流量和热备份备份服务器）。大多数项目可以从这里开始，也可以在开发模式下运行，以便轻松启动没有依赖关系的服务器。
        //这JdbcTokenStore是同一件事的JDBC版本，它将令牌数据存储在关系数据库中。如果您可以在服务器之间共享数据库，则可以使用JDBC版本，如果只有一个，则扩展同一服务器的实例，或者如果有多个组件，则授权和资源服务器。要使用JdbcTokenStore你需要“spring-jdbc”的类路径。


        //这个地方指的是从jdbc查出数据来存储
        clients.inMemory().withClient("android").
                scopes("xx").secret("android").
                authorizedGrantTypes("password", "authorization_code", "refresh_token").
                and().withClient("webapp").scopes("xx").authorizedGrantTypes("implicit");
    }

    /**
     * AuthorizationServerSecurityConfigurer 用来配置令牌端点的安全约束
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()").
                checkTokenAccess("isAuthenticated()");
    }

    /**
     * AuthorizationServerEndpointsConfigurer 用来配置授权以及令牌的访问端点和令牌服务。
     * authenticationManager: 认证管理器，当你选择了资源所有者密码授权类型的时候，请设置这个属性注入一个AuthenticationManager对象
     * userDetailsService:有自己的UserDetailsService接口实现，设置之后
     * authorizationCodeServices:用来设置授权服务，
     * implicitGrantService:设置隐士授权模式
     * tokenGrant:
     *AuthorizationServerEndpointsConfigurer的pathMapping()方法，有两个参数，第一个是默认的URL路径，第二个是自定义的路径
     * WebSecurityConfigurer的实例，可以配置哪些路径不需要保护，哪些需要保护。默认全都保护。
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //authenticationManager直接注入一个授权管理器，开启密码管理
        //userDetailsService()将会启动刷新token授权类型，会判断用户是否存活
        //authorizationCodeServices()AuthorizationCodeServices实例

        //这里使用redis来存储token
        endpoints.authenticationManager(authenticationManager).tokenStore(redisTokenStore());
    }

    /**
     * 令牌存储，
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * Jwt资源令牌转换器,是用非对称加密算法加密
     * 这里不是资源服务器为甚麽要使用资源转换器
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        return new JwtAccessTokenConverter() {
            /**
             * 重写token的增强方法
             * @param accessToken
             * @param authentication
             * @return
             */
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String userName = authentication.getUserAuthentication().getName();
                User user = (User) authentication.getUserAuthentication().getPrincipal();
                Map<String, Object> infoMap = new HashMap<>();
                infoMap.put("userName", userName);
                infoMap.put("roles", user.getAuthorities());
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(infoMap);
                return super.enhance(accessToken, authentication);
            }
        };
    }

    //JwtStore在哪里使用AuthorizationServerTokenServices 这个接口在哪里使用
    @Bean
    private UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("hyf").password("123456").roles("admin").build());
        manager.createUser(User.withUsername("admin").password("Admin").roles("admin").build());
        return manager;
    }
}

/**
 * AuthorizationEndpoint 进行授权的服务,Default URL:/oauth/authorize
 * TokenEndpoint:获取token服务,Default URL:/ouath/token
 * Resource Server:
 *   OAuth2AuthenticationProcessingFilter: 给带有访问令牌的请求加载认证
 *Authorization Server:
 *  @EnableAuthorizationServer: 声明一个认证服务器,当用此注解后，应用启动后将自动生成几个Endpoint:
 *   ouath/authorize:验证
 *   ouath/token:/获取token
 *   ouath/confirm_access:用户授权
 *   ouath/error:认证失败
 *   ouath/check_token:资源服务器用来校验token
 *   oauth/token_key:如果jwt模式则可以用此来从认证服务器获取公钥.
 */

/**
 * AuthorizationServerConfigure包含三种配置：
 *   ClientDetailsServiceConfigurer: client客户端的额信息配置
 *       client包括clientId,secret,scope,authorizedGrantTypesauthorities等:
 *        scope: 表示权限范围，可选项，用户授权页面时进行选择
 *        authorizedGrantTypes:四种授权方式
 *            Authorization Code:用验证获取code，再用code获取token，
 *            lmplicit:隐式授权模式
 *            Client Credentials:用来获取App Access Token
 *            Resrouce Owner Password Credentials:
 *       authorities:授权client权限.    in-memory,JdbcClientDetailsService,jwt等
 *    AuthorizationServerSecurityConfigurer: 声明安全约束，哪些允许访问，哪些不允许访问
 *    AuthorizationServerEndpointsConfigurer 声明授权和token的端点以及token的服务的一些配置信息，比如采用什么存储方式、token的有效期等
 *    client的信息读取:在ClientDetailsServiceConfigurer类里面进行配置，可以有in-memory、jdbc等多种读取方式。备注:了解不同中的读取方式有什莫区别以及怎末去读取。
 *        JdbcClientDetailsService调用，需要传入相应的datasource
 *    如何管理token：
 *      AuthorizationServerTokenServices：声明必要的关于token的操作。
 *        token创建之后，保存，以便以后接受访问令牌的资源可以引用它。
 *        访问令牌用来加载认证。
 *        DefaultTokenServices是对它的默认事现
 *     token的存储方式有三种，InMemoryTokenStore:存放在内存中，不会进行持久化
 *     JdbcTokenStore：存放数据库中
 *     Jwt: json web token,
 *
 *
 * */